package roborally.Pieces;

import java.util.HashSet;
import java.util.Set;
import be.kuleuven.cs.som.annotate.*;
import roborally.Board;
import roborally.util.MoveOutOfBoardException;

/**
 * This is a class of objects that have a Position and an Orientation. An extended instance of HashSet guarantees that all 
 * PositionWithOrientations are unique to avoid memory errors. This is done so that (pwo1 == pwo2) == (pwo1.equals(pwo2) for any
 * PositionWithOrientation pwo1 or pwo2.
 * @version 3.0
 * @author 	Bart Opsomer 2de Bachelor Ingenieurswetenschappen Electrotechniek-Computerwetenschappen 
 * 		   	Peter Opsomer 1ste Master Wiskundige IngenieursTechnieken
 */
@Value
public class PositionWithOrientation {
	private final Position position;
	private final Orientation orientation;
	//The collection of used PositionWithOrientations
	private static final PositionWithOrientation.ExtendedHashSet allPWOs = new PositionWithOrientation.ExtendedHashSet();
	//The implementation of that collection
	private static class ExtendedHashSet extends HashSet<PositionWithOrientation> implements java.lang.Iterable<PositionWithOrientation>{
		// Get the unique position corresponding with (x,y) on board and the given orientation 
		private PositionWithOrientation getPositionWithOrientationAt(long x, long y, Board board,Orientation orientation) {
			Position position = Position.getPosition(x,y,board);
			for(PositionWithOrientation pwo : this) {
				if(pwo.equals(position,orientation) ) {
					return pwo;
				}
			}
			PositionWithOrientation newPos =  new PositionWithOrientation(position, orientation);
			this.add(newPos);
			return newPos;
		}
		private static final long serialVersionUID = 1L;
	}
	/**
	 * Create a new PositionWithOrientation on the Board. Since position should satisfy its class invariants, it is a valid Position.
	 * 
	 * @param position the Position of the PositionWithOrientation
	 * @param orientation the Orientation of the PositionWithOrientation
	 * @post | new.getPosition().equals(position)
	 * @post | new.getOrientation().equals(orientation)
	 */
	@Raw
	private PositionWithOrientation(Position position, Orientation orientation) {
		this.position = position;
		this.orientation = orientation;
	}
	/**
	 * return a PositionWithOrientation with the given orientation without a position
	 * @param orientation the given Orientation
	 * @effect	this(null,orientation)
	 */
	@Raw
	private PositionWithOrientation(Orientation orientation) {
		this(null,orientation);
	}
	/**
	 * Return the desired PositionWithOrientation. It is created if it doesn't exist yet. Else, the existing PositionWithOrientation is returned.
	 * 
	 * @param x the x-coordinate of the (Position of the) PositionWithOrientation
	 * @param y the y-coordinate of the (Position of the) PositionWithOrientation
	 * @param board the Board of the (Position of the) PositionWithOrientation
	 * @param orientation the Orientation of the PositionWithOrientation
	 * @return | result.getX() == x
	 * @return | result.getY() == Y
	 * @return | result.getBoard() == board
	 * @throws IllegalArgumentException
	 * 		| !isValidPosition(board, x,y )
	 */
	public static PositionWithOrientation getPWO(long x, long y, Board board, Orientation orientation) {
		 return allPWOs.getPositionWithOrientationAt(x,y,board,orientation);
	 }
	/**
	 * returns a PositionWithOrientation that has the given orientation but has a null value as position
	 * @param orientation the orientation of the object
	 * @return result.getPosition == null && result.getOrientation == orientation
	 */
	public static PositionWithOrientation getOrientationWithoutPosition(Orientation orientation) {
		return new PositionWithOrientation(orientation);
	}
	/**
	 * @return the Position of this PositionWithOrientation
	 */
	@Basic @Immutable
	public Position getPosition() {
		return position;
	}
	/**
	 * @return  | result.equals(this.getPosition().getBoard() )
	 */
	@Immutable
	public Board getBoard() {
		return this.getPosition().getBoard();
	}
	/**
	 * @return the Orientation of this PositionWithOrientation
	 */
	@Basic @Immutable
	public Orientation getOrientation() {
		return orientation;
	}
	/**
	 * Return whether this PositionWithOrientation is equal to the given Object.
	 * 
	 * @effect | if(other instanceof PositionWithOrientation) then 
	 * 		   |	result ==  equals(second.getPosition(),second.getOrientation())
	 * 		   | else
	 * 		   |	result == false
	 */
	@Override
	public boolean equals(Object other) {
		if(other.getClass() ==  this.getClass()) {
			PositionWithOrientation second = (PositionWithOrientation) other;
			return equals(second.getPosition(),second.getOrientation());
		}
		return false;
	}
	/**
	 * Return whether this PositionWithOrientation is equal to the PositionWithOrientation denoted by the arguments.
	 * The given parameter position may be null.
	 * 
	 * @param position the Position of the PositionWithOrientation
	 * @param orientation the Orientation of the PositionWithOrientation
	 * @return | this.getPosition().equals(position) && this.getOrientation().equals(orientation)
	 */
	public boolean equals(Position position, Orientation orientation) {
		return this.position.equals(position) && this.orientation.equals(orientation);
	}
	/**
	 * Return the next PositionWithOrientation: this is the PositionWithOrientation obtained by moving one step forward.
	 * 
	 * @return  | result.getOrientation().equals(this.getOrientation() )
	 * @return  | result.getPosition().getManhattanDistanceTo(this.getPosition() ) == 1
	 * @effect  | result.equals(orientation.getNextPosition(position) )
	 * @throw  	MoveOutOfBoardException 
	 * 			| !Position.isValidPosition(result.getPosition().getX(), result.getPosition().getY(), result.getPosition().getBoard() )
	 */
	public PositionWithOrientation nextPosition() throws MoveOutOfBoardException {
		return orientation.getNextPosition(position);
	}
	/**
	 * Get a clockwise rotated version of this PositionWithOrientation.
	 * @effect | result.getOrientation().equals(this.getOrientation().turnClockwise() )
	 * @return | result.getPosition().equals(this.getPosition() )
	 */
	public PositionWithOrientation turnClockwise() {
		return getPWO(position.getX(), position.getY(), position.getBoard(), orientation.turnClockwise() );
	}
	/**
	 * Get a counterclockwise rotated version of this PositionWithOrientation.
	 * @effect | result.getOrientation().equals(this.getOrientation().turnClockwise() )
	 * @return | result.getPosition().equals(this.getPosition() )
	 */
	public PositionWithOrientation turnCounterClockwise() {
		return getPWO(position.getX(), position.getY(), position.getBoard(), orientation.turnCounterClockwise() );
	}
	/**
	 * Get all neighbors of this Position where there is no wall and that are not in the given Set. 
	 * @param notThese the PositionWithOrientations that cannot be in the returned Set
	 * @return  | forall(PositionWithOrientation pwo : notThese)
	 * 		    |	!result.contains(pwo)
	 * @return  | forall(PositionWithOrientation pwo : result)
	 * 		    |	pwo.getPosition().getManhattanDistanceTo(this) == 1 && 
	 * 			|	!this.getBoard().hasWallAt(pwo.getPosition() && 
	 * 			|	!this.getBoard().hasRobotAt(pwo.getPosition()) 
	 */
	public Set<PositionWithOrientation> getGoodNeighbours(Set<PositionWithOrientation> notThese) {
		Set<PositionWithOrientation> neighbours = new HashSet<PositionWithOrientation>();

		for(Orientation or : Orientation.values()) {
			try{
				PositionWithOrientation pwo = or.getNextPosition(position);
				addIfGoodNeighbour(pwo,notThese,neighbours);
			}
			catch(MoveOutOfBoardException exc) { }
		}
		return neighbours;
	}
	private void addIfGoodNeighbour(PositionWithOrientation toAdd, Set<PositionWithOrientation> notThese, Set<PositionWithOrientation> neighbours) {
		if (!Position.isValidPosition(toAdd.getBoard(),toAdd.position.getX(), toAdd.position.getY() )) return;
		if (notThese.contains(toAdd) ) return;
		if (toAdd.getBoard().hasWallAt(toAdd.getPosition()) ) return;
		if (toAdd.getBoard().hasRobotAt(toAdd.getPosition()) ) return;
		neighbours.add(toAdd);
		
	}
}
